package com.peking.donations.plugins.oauth2.client.wechart;

import com.peking.donations.plugins.oauth2.client.OAuth2Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.client.filter.state.DefaultStateKeyGenerator;
import org.springframework.security.oauth2.client.filter.state.StateKeyGenerator;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.TreeMap;

/**
 * @author Vigor Yuan
 */
public class WxAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider {

    private StateKeyGenerator stateKeyGenerator = new DefaultStateKeyGenerator();

    private boolean stateMandatory = true;

    String profile;

    @Override
    public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
            throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
            OAuth2AccessDeniedException {

        AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;

        if (request.getAuthorizationCode() == null) {
            logger.warn("authorizationCode is null init redirect");
            if ("prod".equals(profile)) {
                throw getRedirectForAuthorizationThird(resource, request);
            }
            if (request.getStateKey() == null) {
                throw getRedirectForAuthorization(resource, request);
            }
            obtainAuthorizationCode(resource, request);
        }

        logger.debug("authorizationCode is " + request.getAuthorizationCode() + ", begin get access_token");
        OAuth2AccessToken auth2AccessToken;
        try {
            auth2AccessToken = retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
                    getHeadersForTokenRequest(request));
        } catch (Exception e) {
            logger.error("获取oauth2 access_token 异常"+e.getMessage());
            throw new OAuth2Exception(e.getMessage());
        }
        return auth2AccessToken;

    }

    @Override
    public OAuth2AccessToken refreshAccessToken(OAuth2ProtectedResourceDetails resource,
            OAuth2RefreshToken refreshToken, AccessTokenRequest request) throws UserRedirectRequiredException {
        MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
        form.add("appid", resource.getClientId());
        form.add("grant_type", "refresh_token");
        form.add("refresh_token", refreshToken.getValue());
        try {
            return retrieveToken(request, resource, form, getHeadersForTokenRequest(request));
        } catch (OAuth2AccessDeniedException e) {
            throw getRedirectForAuthorization((AuthorizationCodeResourceDetails) resource, request);
        }
    }

    @Override
    protected String getAccessTokenUri(OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form) {
        String accessTokenUri = resource.getAccessTokenUri();
        if ("refresh_token".equals(form.get("grant_type"))) {
            accessTokenUri = ((OAuth2Resource) resource).getRefreshAccessTokenUri();
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Retrieving token from " + accessTokenUri);
        }

        StringBuilder builder = new StringBuilder(accessTokenUri);

        if (getHttpMethod() == HttpMethod.GET) {
            String separator = "?";
            if (accessTokenUri.contains("?")) {
                separator = "&";
            }

            for (String key : form.keySet()) {
                builder.append(separator);
                builder.append(key + "={" + key + "}");
                separator = "&";
            }
        }

        return builder.toString();
    }

    private MultiValueMap<String, String> getParametersForTokenRequest(AuthorizationCodeResourceDetails resource,
            AccessTokenRequest request) {

        MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
        form.set("appid", resource.getClientId());
        form.set("secret", resource.getClientSecret());
        form.set("code", request.getAuthorizationCode());
        form.set("grant_type", "authorization_code");
        logger.debug("current profile is "+profile);
        Object preservedState = request.getPreservedState();
        if (!"prod".equals(profile)) {
            logger.debug("prod .equals profile false");
            if (request.getStateKey() != null || stateMandatory) {
                // The token endpoint has no use for the state so we don't send
                // it
                // back, but we are using it
                // for CSRF detection client side...
                if (preservedState == null) {
                    throw new InvalidRequestException(
                            "Possible CSRF detected - state parameter was required but no state could be found");
                }
            }
        }

        // Extracting the redirect URI from a saved request should ignore the
        // current URI, so it's not simply a call to
        // resource.getRedirectUri()
        String redirectUri = null;
        // Get the redirect uri from the stored state
        if (!"prod".equals(profile) && preservedState instanceof String) {
            // Use the preserved state in preference if it is there
            // TODO: treat redirect URI as a special kind of state (this is a
            // historical mini hack)
            redirectUri = String.valueOf(preservedState);
        } else {
            redirectUri = resource.getRedirectUri(request);
        }

        if (redirectUri != null && !"NONE".equals(redirectUri)) {
            form.set("redirect_uri", redirectUri);
        }

        return form;

    }

    private HttpHeaders getHeadersForTokenRequest(AccessTokenRequest request) {
        HttpHeaders headers = new HttpHeaders();
        // No cookie for token request
        return headers;
    }

    private UserRedirectRequiredException getRedirectForAuthorization(AuthorizationCodeResourceDetails resource,
            AccessTokenRequest request) {

        // we don't have an authorization code yet. So first get that.
        TreeMap<String, String> requestParameters = new TreeMap<String, String>();
        String redirectUri = resource.getRedirectUri(request);
        try {
            redirectUri = URLEncoder.encode(redirectUri, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        requestParameters.put("appid", resource.getClientId());
        requestParameters.put("redirect_uri", redirectUri);
        requestParameters.put("response_type", "code");
        requestParameters.put("scope", "snsapi_userinfo");

        UserRedirectRequiredException redirectException = new UserRedirectRequiredException(
                resource.getUserAuthorizationUri(), requestParameters);

        String stateKey = stateKeyGenerator.generateKey(resource);
        redirectException.setStateKey(stateKey);
        request.setStateKey(stateKey);
        redirectException.setStateToPreserve(redirectUri);
        request.setPreservedState(redirectUri);

        return redirectException;

    }

    private UserRedirectRequiredException getRedirectForAuthorizationThird(AuthorizationCodeResourceDetails resource,
            AccessTokenRequest request) {
        TreeMap<String, String> requestParameters = new TreeMap<String, String>();
        UserRedirectRequiredException redirectException = new UserRedirectRequiredException(
                resource.getUserAuthorizationUri(), requestParameters);
        logger.debug("prod oauth2 redirect url " + redirectException.getRedirectUri());
        return redirectException;

    }

    public String getProfile() {
        return profile;
    }

    public void setProfile(String profile) {
        this.profile = profile;
    }
}
